上一篇講到 Component 拆分之後,很快會遇到另一個問題。
假設現在有:
Page
↓
Section
↓
Panel
↓
Button
而真正需要某份資料的,其實只有最下面的:
Button
但資料一開始存在最上面的:
Page
那最直覺的做法就是一路傳 Props:
Page
↓ props
Section
↓ props
Panel
↓ props
Button
看起來好像沒問題。
但如果中間的:
Section
Panel
根本不需要這份資料,只是負責「幫忙往下傳」,
程式就開始變得有點奇怪。
假設最上層有:
const user = {
name: "小潔",
};
然後:
function Page() {
const user = {
name: "小潔",
};
return <Section user={user} />;
}
Section 再傳:
function Section({ user }) {
return <Panel user={user} />;
}
Panel 再傳:
function Panel({ user }) {
return <UserButton user={user} />;
}
最後:
function UserButton({ user }) {
return (
<button>
{user.name}
</button>
);
}
資料流:
Page
│
│ user
↓
Section
│
│ user
↓
Panel
│
│ user
↓
UserButton
問題是:
Section
Panel
其實根本沒有使用 user。
它們只是:
收到 → 再傳下去。
這種一層一層把 Props 往下傳的情況,常被稱為:
Props Drilling
不是說:
Props 傳很多層一定錯。
而是當中間很多 Component 根本不需要這份資料時,維護成本就可能開始增加。
例如本來:
<Section user={user} />
後來又增加:
<Section
user={user}
permission={permission}
currency={currency}
timezone={timezone}
/>
接著 Section 自己沒用,又全部傳:
<Panel
user={user}
permission={permission}
currency={currency}
timezone={timezone}
/>
再下一層又繼續傳。
最後可能變成:
Page
↓
4 個 Props
Section
↓
4 個 Props
Panel
↓
4 個 Props
真正需要資料的 Child
這時候我以前就會開始疑惑:
為什麼中間這麼多 Component 都要知道這些東西?
我自己後來比較容易理解 Context 的方式,是把它想成一個佈告欄。
以前:
Page
↓
把資料交給 Section
Section
↓
再交給 Panel
Panel
↓
再交給 Button
很像一張紙一直往下傳。
Context 比較像:
Parent
↓
把共享資料放到一個範圍裡
┌───────────────┐
│ Context │
│ │
│ user │
│ permission │
└───────────────┘
↑
範圍內需要的人
自己來讀
所以中間的 Component:
Section
Panel
如果不需要 user,就不用只是為了傳遞而收到它。
先看一個簡單版本:
import {
createContext,
useContext,
} from "react";
const UserContext = createContext(null);
這裡:
UserContext
可以先理解成:
建立一個放共享資料的位置。
接著 Provider:
function Page() {
const user = {
name: "小潔",
};
return (
<UserContext.Provider value={user}>
<Section />
</UserContext.Provider>
);
}
這裡的:
value={user}
就是把資料提供給 Provider 範圍裡面的 Component。
流程可以想成:
Page
↓
UserContext.Provider
↓
提供 user
↓
底下的 Component 可以讀
真正需要資料的地方:
function UserButton() {
const user = useContext(UserContext);
return (
<button>
{user.name}
</button>
);
}
這裡:
useContext(UserContext)
可以先理解成:
我要去這個 Context 裡拿目前共享的資料。
所以三個角色可以先記成:
createContext
→ 建立 Context
Provider
→ 把資料提供出去
useContext
→ 把資料拿進來使用
原本:
Page
↓ user
Section
↓ user
Panel
↓ user
UserButton
改成 Context:
Page
│
│ user
↓
UserContext.Provider
│
├─ Section
│ └─ Panel
│ └─ UserButton
│ ↑
│ useContext()
│
└────────────┘
中間:
Section
Panel
不需要 user,
就不必為了「幫忙傳資料」而接 Props。
這就是 Context 最容易理解的價值之一。
這裡是我覺得很重要的一點。
學到 Context 後,很容易開始覺得:
那我是不是什麼都放 Context 就好了?
其實不是。
如果只有:
Parent
↓
Child
而且 Child 本來就需要那份資料:
<ProductInfo products={products} />
這樣 Props 本身就非常清楚。
沒有必要為了少寫一個 Props,就建立 Context。
例如:
DetailPage
↓
ProductInfo
只差一層,而且:
ProductInfo
本來就需要 products。
那:
<ProductInfo products={products} />
反而很好讀。
我目前比較會在這些狀況下考慮:
同一份資料
↓
很多不同 Component 都需要
或
資料需要經過很多層
↓
中間 Component 根本不用
↓
只是一直幫忙傳 Props
例如:
DetailPage
│
├─ Header
│
├─ BasicInfo
│
├─ ProductInfo
│
├─ DocumentInfo
│
└─ ActionArea
如果很多地方都需要:
detail
permission
currentMode
這時候就可以開始思考:
這些是不是適合放到一個共享的 Context?
而不是一直變成:
<BasicInfo
detail={detail}
permission={permission}
currentMode={currentMode}
/>
<ProductInfo
detail={detail}
permission={permission}
currentMode={currentMode}
/>
<DocumentInfo
detail={detail}
permission={permission}
currentMode={currentMode}
/>
我以前看到:
useContext(...)
第一個反應通常是:
又來一個 Hook。
但後來真的碰到比較大的頁面,拆成很多 Component 之後,才發現問題不是:
「我要不要學 Context?」
而是:
這份資料很多地方都要
↓
如果一直用 Props 傳
↓
中間很多 Component 只是轉手
↓
資料依賴開始變得很亂
這時候 Context 才開始有實際意義。
所以對我來說,學 Context 最重要的不是先背:
createContext
Provider
useContext
而是先知道:
它是為了解決什麼資料傳遞問題而存在。
Context 雖然可以減少 Props Drilling,但它也不是免費的。
如果什麼東西都塞進 Context:
user
permission
form
loading
modal
table
filters
detail
button
...
最後可能變成另一種問題:
Component 表面上沒有 Props,但其實背後偷偷依賴很多 Context 資料。
這時候反而更難看出:
這個 Component 到底需要什麼?
所以我現在比較喜歡把它想成:
Props
→ 明確、直接的資料依賴
Context
→ 某個範圍內需要共享的資料
不是:
Props 很麻煩
↓
全部改 Context
如果資料只是:
Parent
↓
Child
而且 Child 本來就需要
→ Props 很適合
如果變成:
Parent
↓
Middle A
↓
Middle B
↓
Middle C
↓
真正需要資料的 Child
而中間都只是轉手
→ 可以開始思考 Context
所以看到 Context 時,我現在會先問:
這份資料為什麼要共享?
↓
有哪些 Component 需要?
↓
如果不用 Context,
是不是會產生很多沒有意義的 Props 傳遞?
而不是看到:
useContext()
就先害怕它。
Context 本質上還是在處理我們這幾天一直追的同一件事:
資料從哪裡來?誰需要它?又該怎麼到那裡?
下一篇,我想繼續接一個 React 裡非常容易搞混的東西:
useEffect 到底什麼時候會執行?為什麼有時候 API 一進頁面就自己打出去了?